这是JS 原生方法原理探究系列的第二篇文章。本文会介绍如何实现 Object.create()
方法。关于这个方法的具体用法,MDN 已经描述得很清楚了,这里我们只做简单的介绍,具体的重点在于如何模拟实现。
语法简介
调用:Object.create ( proto , propertiesObject )
返回: 一个新的实例对象
调用这个方法的时候接受两个参数,第一个参数作为返回对象的 __proto__
,这个参数只能是 null 或者对象(而且不能是基本类型的包装对象)。
第二个参数作为返回对象的属性描述,它和 Object.defineProperties()
的第二个参数形式是一样的:
{
propertyA: {
value: xxx,
configurable: xxx,
enumerable: xxx,
writable: xxx
},
propertyB: {...},
propertyC: {...}
}
这个参数的每一个属性都会作为返回对象的属性,而属性值则是相应属性的特性描述(该属性的属性值、是否可读、是否可枚举、是否可配置)。第二个参数只能是对象或者 undefined(表示没有传第二个参数),不能是 null。
ES 规范
对于 Object.create()
的具体实现,规范中其实已经描述得很清楚,可以进入http://es5.github.io/#x15.2.3.5查看:
我简单翻译一下这段话:
create()
方法会创建一个具有指定原型的新对象,当调用该方法的时候,会有如下步骤:
- 如果传入的参数
O
不是对象也不是null
,抛出 TypeError 错误 - 令
obj
作为调用new Object()
方法所创建的新对象 - 将
obj
的内部属性[[prototype]]
设置为O
- 如果提供了第二个参数
Properties
,且不是undefined
,则调用Object.defineProperties
方法并传入obj
和Properties
作为参数,从而为obj
添加它自己的属性 - 返回
obj
可以说,整个过程是一目了然的,我们实现的时候也只需要按照上述步骤实现即可。
代码实现
我们先看第一种实现:
Object.create = function(proto,propertiesObject){
if(typeof proto != 'object' && proto !== null){
throw new Error('the first param must be an object or null')
}
if(typeof propertiesObject === null){
throw 'TypeError'
}
let obj = {}
obj.__proto__ = proto
if(propertiesObject){
Object.defineProperties(obj,propertiesObject)
}
return obj
}
基本上没有什么大问题。不过,我们要留意两个地方:
- 在这个实现中,没有检测第一个参数是不是基本类型的包装对象,只要传进来的参数是对象,我们就认为是合法的
- 当传入 null 也即
Object.create(null)
的时候,我们实际上创建了一个很纯粹的空对象,这个对象的原型直接就是 null,Object.prototype
甚至没有出现在该对象的原型链中,这意味这个对象不会继承 Object 的任何方法。
此外,你还可能在其他地方看到类似下面这样的实现:
具体实现如下:
Object.create = function(proto,propertiesObject){
if(typeof proto != 'object' && proto !== null){
throw new Error('the first param must be an object or null')
}
if(propertiesObject === null){
throw 'TypeError'
}
function F(){}
F.prototype = proto
const obj = new F()
// 处理传参 null 的情况
if(proto === null){
obj.__proto__ = proto
}
if(propertiesObject){
Object.defineProperties(obj,propertiesObject)
}
return obj
}
这个实现和前面的实现有一个很关键的区别:代码中单独处理了传参 proto
为 null
的情况。可能你会觉得很奇怪:当 proto
为 null
的时候,F.prototype = proto
的效果和 obj.__proto__ = proto
应该是一样的,为什么还要在这种情况下执行一遍 obj.__proto__ = proto
呢?这似乎说明,用 null 重写 F 的原型后,新创建的实例的 __proto__
并不是 null —— 事实上确实不是。
关于调用构造函数时会执行的操作,规范明确提到了这一点:
If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4.
由于我们这里是通过 new 构造函数的方式创建新对象(而不是像之前那样通过对象字面量的形式),所以在 new F 的时候,内部会检测 F 的原型是不是对象,如果不是对象,那么会把实例的 __proto__
链接到内建的 Object.prototype
。因此,这里新创建的实例的 __proto__
还真不是 null。
但根据 Object.create
的实现规范,这里必须让实例的 __proto__
指向 null,所以才需要执行 obj.__proto__ = proto
去手动设置对象原型。
当然,如果我们像第一个实现那样,直接去设置对象的 __proto__
,而不是采用构造函数的方式,就不存在这个问题了。